using System;
using System.Collections.Generic;
using System.Linq;

using Atomic.Libraries;
using Atomic.Libraries.Plotting.Functions;
using Atomic.Structures;

namespace Atomic.Thermodynamics
{
	[Serializable]
	public class CubicSpline
	{
		private CubicSpline(IEnumerable<Atom> atoms, IEnumerable<CompositionMapping> energies, CubicSplineInterpolationFunction function)
		{
			Atoms = new ReadOnlyList<Atom>(atoms);
			Energies = new ReadOnlyList<CompositionMapping>(energies);
			Function = function;
		}

		public static CubicSpline Fit(IEnumerable<CompositionMapping> energies)
		{
			if (energies == null)
			{
				throw new ArgumentNullException();
			}

			// Concatenate arrays of atoms and determine distinct atoms.
			Atom[] atoms = energies.SelectMany(c => c.Composition.Atoms).Distinct().ToArray();

			if (atoms.Length != 2)
			{
				throw new NotSupportedException();
			}

			energies = energies.OrderBy(c => c.Composition.Fraction(atoms[0]));
			CubicSplineInterpolationFunction function = CubicSplineInterpolationFunction.CreateNaturalCubicSpline(energies.Select(c => c.Composition.Fraction(atoms[0])).ToArray(), energies.Select(c => c.Value).ToArray());

			return new CubicSpline(atoms, energies, function);
		}

		public static CubicSpline FitGroundState(IEnumerable<CompositionMapping> energies)
		{
			if (energies == null)
			{
				throw new ArgumentNullException();
			}

			// Select lowest energy for each composition.
			return Fit(energies.GroupBy(e => e.Composition).Select(g => g.OrderBy(cm => cm.Value).First()));
		}

		private void CheckAtoms(Composition composition)
		{
			foreach (Atom atom in composition.Atoms)
			{
				if (atom == Atom.Undefined)
				{
					// Allow this for testing purposes.
					continue;
				}

				if (!Atoms.Contains(atom))
				{
					throw new ArgumentException("Composition contains atom not included in the fitting.");
				}
			}
		}

		public double Energy(Composition composition)
		{
			CheckAtoms(composition);

			double x = composition.Fraction(Atoms[0]);
			return Function.Value(x);
		}

		public ReadOnlyList<Atom> Atoms
		{
			get;
			private set;
		}

		public ReadOnlyList<CompositionMapping> Energies
		{
			get;
			private set;
		}

		public CubicSplineInterpolationFunction Function
		{
			get;
			private set;
		}
	}
}
